Skip to content

Hooks: Fix resort_active_iterations() skipping next priority on self-removal #10949

Open
mrcasual wants to merge 1 commit intoWordPress:trunkfrom
mrcasual:fix/wp-hook-resort-active-iterations-skip
Open

Hooks: Fix resort_active_iterations() skipping next priority on self-removal #10949
mrcasual wants to merge 1 commit intoWordPress:trunkfrom
mrcasual:fix/wp-hook-resort-active-iterations-skip

Conversation

@mrcasual
Copy link
Copy Markdown

@mrcasual mrcasual commented Feb 17, 2026

Trac ticket: https://core.trac.wordpress.org/ticket/64653

When a callback removes itself during do_action() / apply_filters() and is the sole callback at its priority, resort_active_iterations() positions the internal array pointer one step too far. The main loop's next() call then skips the following priority entirely.

Steps to reproduce

<?php

add_action( 'test_hook', '__return_true', 10 );

add_action( 'test_hook', function () {
    remove_action( 'test_hook', __FUNCTION__, 50 );
}, 50 );

add_action( 'test_hook', function () {
    error_log( 'This never runs' );
}, 100 );

do_action( 'test_hook' ); // Priority 100 is silently skipped.

The bug requires: (1) a lower priority exists, (2) the removed callback is the only one at its priority.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 17, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props mrcasual, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@mrcasual mrcasual force-pushed the fix/wp-hook-resort-active-iterations-skip branch from 90f0d19 to 40ea968 Compare February 17, 2026 00:33
@github-actions
Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

*
* @ticket 64653
*/
public function test_self_removing_callback_does_not_skip_next_priority() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm this test fails without the fix applied:

1) Tests_Hooks_DoAction::test_self_removing_callback_does_not_skip_next_priority
Priority 100 should not be skipped when priority 50 removes itself during iteration.
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
 Array &0 (
     0 => 10
     1 => 50
-    2 => 100
 )

/var/www/tests/phpunit/tests/hooks/doAction.php:331

*
* @ticket 64653
*/
public function test_self_removing_callback_at_lowest_priority() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test does not fail without the fix applied.

Copy link
Copy Markdown
Author

@mrcasual mrcasual Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test does not fail without the fix applied.

Yup, that's expected. This test covers the edge case where the self-removing callback is at the lowest priority, which takes a different code path in resort_active_iterations() that already worked correctly. I included it as a regression guard since there was no coverage for that path if it's ever refactored.

…f-removal.

When a callback removes itself during `do_action()`/`apply_filters()` and is the
sole callback at its priority, `resort_active_iterations()` repositions the
internal array pointer one position too far. The main loop's `next()` call then
skips the following priority entirely.

The fix detects when the current priority was removed (the pointer advanced
past it) and calls `prev()` so that `next()` in the calling loop lands on the
correct priority.

Fixes #64653.
@mrcasual mrcasual force-pushed the fix/wp-hook-resort-active-iterations-skip branch from 40ea968 to 9ca834c Compare February 20, 2026 16:08
@mrcasual
Copy link
Copy Markdown
Author

@westonruter, what's the process for moving this forward?

@westonruter westonruter requested a review from Copilot March 26, 2026 18:38
@westonruter
Copy link
Copy Markdown
Member

@mrcasual Once the 7.0 branch is made and trunk is open for commits, we can move this forward for WordPress 7.1.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a WP_Hook iteration bug where removing the only callback at the current priority during do_action() / apply_filters() could cause the next priority to be skipped due to an off-by-one internal pointer reposition in resort_active_iterations().

Changes:

  • Adjust WP_Hook::resort_active_iterations() to step the iteration pointer back when the current priority was removed, so the main loop’s next() lands on the correct next priority.
  • Add PHPUnit regression tests covering self-removal at a middle priority and at the lowest priority during do_action().

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/wp-includes/class-wp-hook.php Fixes pointer repositioning in resort_active_iterations() when the current priority is removed mid-iteration.
tests/phpunit/tests/hooks/doAction.php Adds regression tests to ensure self-removal does not skip subsequent priorities during actions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +306 to +332
public function test_self_removing_callback_does_not_skip_next_priority() {
$hook = new WP_Hook();
$hook_name = __FUNCTION__;
$log = array();

$callback_10 = function () use ( &$log ) {
$log[] = 10;
};

// Callback that removes itself -- the only callback at priority 50.
$self_removing = function () use ( &$log, &$self_removing, $hook, $hook_name ) {
$hook->remove_filter( $hook_name, $self_removing, 50 );
$log[] = 50;
};

$callback_100 = function () use ( &$log ) {
$log[] = 100;
};

$hook->add_filter( $hook_name, $callback_10, 10, 0 );
$hook->add_filter( $hook_name, $self_removing, 50, 0 );
$hook->add_filter( $hook_name, $callback_100, 100, 0 );

$hook->do_action( array() );

$this->assertSame( array( 10, 50, 100 ), $log, 'Priority 100 should not be skipped when priority 50 removes itself during iteration.' );
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regression tests added here cover the do_action() path, but the PR description indicates the bug also affects apply_filters(). Consider adding an equivalent test in tests/phpunit/tests/hooks/applyFilters.php to ensure the behavior is covered when $this->doing_action is false (different argument handling) and to prevent future regressions specific to apply_filters().

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doing_action flag only affects argument threading at line 338 ($args[0] = $value) - it's never read by resort_active_iterations(). Both do_action() and apply_filters() share the same iteration loop, so these tests already cover both paths. The pointer arithmetic is identical regardless of entry point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants